// Copyright (C) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using Microsoft.Win32;
using System.Text.RegularExpressions;
using System.Threading;
using System.IO;

namespace Microsoft.VSPowerToys.VSCmdShell.Hosts
{
    /// <summary>
    /// Implementation of IShellHost for Windows Command Shell (cmd.exe)
    /// </summary>
    /// <remarks>
    /// To host cmd.exe, an actual cmd.exe process is started where its output, input and error streams are redirected to stream properties
    /// required by IShellHost.To issue commands to shell, that command is directly written in to input stream. 
    /// 
    /// When we keep a running copy of cmd.exe, we gain the capability to allow environment variables that user set during shell interaction
    /// to be persistent. But we are not allowed to access those environment variables using Process object, thus ExpandEnvironmentVariables
    /// method is not implemented for cmd.exe host.
    /// </remarks>
    public class CommandShellHost : IShellHost
    {

        #region Private Variables

        /// <summary>
        /// Object referring to running copy of cmd.exe
        /// </summary>
        private Process shellProcess;

        private byte[] errorBuffer = new byte[1024];

        private byte[] outputBuffer = new byte[1024];

        /// <summary>
        /// Shell UI interface object
        /// </summary>
        private IShellUI shellUI;

        /// <summary>
        /// Last command executed by user
        /// </summary>
        private string lastCommand;

        /// <summary>
        /// Object to be used for thread syncing
        /// </summary>
        private object syncObject = new Object();
        #endregion

        #region Properties

        private System.IO.StreamWriter InputStream
        {
            get
            {
                if (this.shellProcess != null)
                {
                    return this.shellProcess.StandardInput;
                }
                else
                {
                    throw new InvalidOperationException();
                }
            }
        }

        #endregion

        #region IShellHost Members

        public string ShellName
        {
            get
            {
                return "Windows Command Shell (cmd.exe)";
            }
        }

        public char VisualStudioCommandEscapeCharacter
        {
            get
            {
                return '!';
            }
        }

        public void ExecuteCommand(string command)
        {
            lastCommand = command;

            //We need to convert the input string into the proper encoding for cmd.exe (ibm850)
            StreamWriter writer = new StreamWriter(this.InputStream.BaseStream, Encoding.GetEncoding("ibm850"));
            writer.WriteLine(command);
            writer.Flush();
        }

        /// <remarks>
        /// Currently this is not implemented completely for cmd.exe host because there is no
        /// way to access environment variables within the shell process
        /// without executing: echo input command on the shell itself.
        /// 
        /// Currently only the environment variables from VS process is being read
        /// </remarks>
        public string ExpandEnvironmentVariables(string input)
        {
            return System.Environment.ExpandEnvironmentVariables(input);
        }

        public void InitializeShellProcess(IShellUI ui, string startingDirectory)
        {
            lock (this.syncObject)
            {
                this.shellUI = ui;

                //HACK: Polling from the registry to build the tools directory for the VSVars.bat command line init
                //If this is to work with Whidbey or 2002 we should make it agnostic and find a way to get this from
                //the app object in the DTE. 
                RegistryKey rk = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\VisualStudio\\8.0");
                string installDir = (String)rk.GetValue("InstallDir");
                installDir = installDir.Substring(0, installDir.Length - 5);
                installDir = installDir + "\\tools\\";

                // Create cmd.exe, turning off the window and redirecting I/O to us
                ProcessStartInfo info = new ProcessStartInfo("cmd.exe");

                //TODO: make this just the default option for an arguements option
                string argument = "/k \"\"" + installDir + "vsvars32.bat\"";
                if (!string.IsNullOrEmpty(startingDirectory) && System.IO.Directory.Exists(startingDirectory))
                {
                    argument += " & pushd \"" + startingDirectory + "\"\"";
                }
                else
                {
                    argument += "\"";
                }
                info.Arguments = argument;


                info.CreateNoWindow = true;
                info.RedirectStandardError = true;
                info.RedirectStandardInput = true;
                info.RedirectStandardOutput = true;
                info.UseShellExecute = false;
                info.StandardOutputEncoding = Encoding.GetEncoding("ibm850");
                info.StandardErrorEncoding = Encoding.GetEncoding("ibm850");
                this.shellProcess = System.Diagnostics.Process.Start(info);
                this.shellProcess.EnableRaisingEvents = true;
                this.InputStream.AutoFlush = true;
                this.shellProcess.Exited += new EventHandler(ProcessExited);
                this.ErrorBeginRead();
                this.OutputBeginRead();
            }
        }

        /// <summary>
        /// This method is called when shell has exited and it should be restarted
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void ProcessExited(object sender, EventArgs e)
        {
            lock (this.syncObject)
            {
                this.shellProcess.Close();
                this.InitializeShellProcess(this.shellUI, null);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="currentLine"></param>
        /// <returns></returns>
        /// <remarks>This way depends on the shell prompt being set as default.</remarks>
        public string GetCurrentWorkingDirectory(string currentLine)
        {
            if (currentLine == null)
            {
                throw new ArgumentNullException("currentLine");
            }
            currentLine = currentLine.Substring(0, currentLine.IndexOf(@">"));
            return currentLine;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (shellProcess != null && !shellProcess.HasExited)
            {
                shellProcess.EnableRaisingEvents = false;
                shellProcess.Kill();
            }
        }

        #endregion

        #region Methods - Output, Error Stream Handling

        private void ErrorBeginRead()
        {
            this.shellProcess.StandardError.BaseStream.BeginRead(errorBuffer, 0, errorBuffer.Length, new AsyncCallback(this.OnErrorInput), this.shellProcess);
        }

        private void OutputBeginRead()
        {
            this.shellProcess.StandardOutput.BaseStream.BeginRead(outputBuffer, 0, outputBuffer.Length, new AsyncCallback(this.OnOutputInput), this.shellProcess);
        }

        private void OnErrorInput(IAsyncResult result)
        {
            while (!Monitor.TryEnter(this.syncObject, 500)) { }
            try
            {
                if (result.AsyncState != this.shellProcess) return;
                // Dump the characters to the text box
                int cb = this.shellProcess.StandardError.BaseStream.EndRead(result);
                if (cb > 0)
                {
                    string s = this.shellProcess.StandardError.CurrentEncoding.GetString(errorBuffer, 0, cb);

                    // prepare for ipconfig.exe and so on.
                    s = s.Replace("\r\n", "\n");
                    s = s.Replace("\r", "");
                    s = s.Replace("\n", "\r\n");

                    this.shellUI.WriteLine(s);

                    // Start another read
                    ErrorBeginRead();
                }
            }
            finally
            {
                Monitor.Exit(this.syncObject);
            }
        }

        private void OnOutputInput(IAsyncResult result)
        {
            while (!Monitor.TryEnter(this.syncObject, 500)) { }
            try
            {
                if (result.AsyncState != this.shellProcess) return;
                int cb = this.shellProcess.StandardOutput.BaseStream.EndRead(result);
                if (cb > 0)
                {
                    string s = this.shellProcess.StandardOutput.CurrentEncoding.GetString(outputBuffer, 0, cb);

                    // prepare for ipconfig.exe and so on.
                    s = s.Replace("\r\n", "\n");
                    s = s.Replace("\r", "");
                    s = s.Replace("\n", "\r\n");

                    if (lastCommand != null)
                    {
                        lastCommand += "\r\n";
                    }

                    if (!s.Equals(lastCommand))
                    {
                        this.shellUI.Write(s);
                        lastCommand = null;
                    }

                    // Start another read
                    OutputBeginRead();
                }
            }
            finally
            {
                Monitor.Exit(this.syncObject);
            }
        }
        #endregion
    }
}